<?php

function new_inst ($name, $args=array(), $implement=NULL)
{
  $td = TypeDomain::instance();

	return $td->new_inst($name, $args, $implement);
} // end of function new_object
function new_type ($struct)
{
	$td = TypeDomain::instance();

	return $td->new_type($struct);
} // end of function new_type
function get_type ($name)
{
	$td = TypeDomain::instance();

	return $td->getType($name);
} // end of function get_type

function has_type ($type, $other)
{
	$td = TypeDomain::instance();

	$status = NULL;
	if (is_string($type))  $type = $td->getType($type);
	if (is_string($other)) $other = $td->getType($other);
	if ($type && $other) {
		foreach ($type->getTypeRefs() as $t) {
			if ($t == $other) {
				$status = True;
				break;
			}
		}
	}
	if (!isset($status)) $status = false;
	return $status;
} // end of function isa

function is_script ($filename)
/**
 * Узнать, является-ли файл скриптом, учитывая include_path.
 * 
 * http://bugs.php.net/bug.php?id=6932
 *
 * @param mixed $filename имя файла
 * @access public
 * @return boolean true если можно include, require и т.п., иначе false.
 */
{
	if ($fp = @fopen($filename, 'r', 1)) {
		fclose($fp);
		return true;
	}
	return false;
} // end of function is_script

class KarniTypeLoader extends TypeLoader
{
	public function __construct ($config_dir)
	{
		$this->config_dir = $config_dir;
	} // end of __construct
	public function load ($name)
	{
		if ($p = strpos($name, '.')) $name = substr($name, 0,$p);
		$name = strtolower($name);
		$path = $this->config_dir . '/'.$name.'.php';
		if (is_script($path)) {
			$self = array();
			include $path;
		} else {
			$self = NULL;
		}
		return $self;
	} // end of method load
}
class DocTypeLoader extends TypeLoader
{
	public function __construct ($config_file)
	{
		$this->config_file = $config_file;
	} // end of __construct
	public function load ($name)
	{
		if ($p = strpos($name, '.')) $name = substr($name, 0,$p);
		$name = strtolower($name);

		if (!isset($this->doctypes)) {
			if (is_script($this->config_file)) {
				$docs = array();
				$self = array();
				include $this->config_file;
				foreach ($self as $dname => $dinfo) {
					$doc = $dinfo;
					$doc['name'] = $dname;
					$doc['type'] = 'object';
					if (isset($doc['fields'])) {
						$fields = array();
						foreach ($doc['fields'] as $fname => $finfo) {
							$field = $finfo;
							$field['name'] = $fname;
							$field['type'] = $finfo['db_type'];
							if (array_key_exists('db_name',    $finfo)) $field['db']['name'] = $finfo['db_name'];
							if (array_key_exists('db_type',    $finfo)) $field['db']['type'] = $finfo['db_type'];
							if (array_key_exists('db_size',    $finfo)) $field['db']['size'] = $finfo['db_size'];
							if (array_key_exists('db_default', $finfo)) $field['db']['default'] = $finfo['db_default'];
							$fields[] = $field;
						}
						$doc['fields'] = $fields;
					}
					$docs[$dname] = $doc;
				}
				$this->doctypes = $docs;
			}
		}
		if (!array_key_exists($name, $this->doctypes)) $this->doctypes[$name] = NULL;
		return $this->doctypes[$name];
	} // end of method load
}



abstract class TypeLoader
{
	abstract public function load ($name);
}

class TypeDomain
{
	static public $instance = NULL;
	public $types = array();
	private $loaders = array();

	public function getType ($name, $autoload=true)
	{
		$name = strtolower($name);
		if (!array_key_exists($name, $this->types)) {
			if ($autoload) { $this->load($name); }
			if (!array_key_exists($name, $this->types)) { $this->types[$name] = NULL; }
		}
		return $this->types[$name];
	} // end of function getType
	public function setType ($name, $type)
	{
		$name = strtolower($name);
		if (isset($this->types[$name])) {
			error(__METHOD__."() type '$name' exists");
		} else {
			$this->types[$name] = $type;
			$type->setName($name);
		}
		return $this;
	} // end of function setType

	public function cloneType ($name, $base_name)
	{
		$res = NULL;
		if ($t = $this->getType($base_name)) {
			$res = clone $t;
			$res->addTypeRef($t);
			$this->setType($name, $res);
		}
		return $res;
	} // end of function cloneType
	public function aliasType ($name, $base_name)
	{
		$res = NULL;
		if ($res = $this->getType($base_name)) {
			$this->setType($name, $res);
		}
		return $res;
	} // end of function cloneType

	public function new_inst ($name, $args=NULL, $implement=NULL)
	{
		$o = NULL;
		if (is_string($name)) $type = $this->getType($name);
		else { $type = $name; }
		if ($type) {
			$o = $type->create($args, $implement);
		} else {
			error(__METHOD__."() type '{$name}' not found");
		}
		return $o;
	} // end of function new_inst
	
	public function new_type ($struct, $autoload=true)
	{
		$type_name = $struct['name'];
		$base_name = $struct['type'];

		debug(__METHOD__."() '$type_name' :: '$base_name'");
		if ($type = $this->getType($type_name, $autoload)) {
			error(__METHOD__."() type '$type_name' already exists");
		} elseif ($type = $this->cloneType($type_name, $base_name)) {
			$type->init($struct);
		} else {
			error(__METHOD__."() type '$base_name' not defined");
		}

		return $type;
	} // end of method new_type

	public function addLoader ($loader)
	{
		$this->loaders[] = $loader;
	} // end of method addLoader
	public function load ($name)
	{
		$type = NULL;
		$d = substr($name, 0, strpos($name, '.'));
		if ($d && isset($this->types[$d])) return NULL;

		foreach ($this->loaders as $loader) {
			if ($struct = $loader->load($name)) {
				$type = $this->new_type($struct, false);
				break;
			}
		}
		return $type;
	} // end of method load

	static public function instance ()
	{
		if (!isset(self::$instance)) {
			self::$instance = new TypeDomain();
		}
		return self::$instance;
	} // end of function instance
}



class TAtom
{
	public $name;
	public $type_ref = array();
	public $meta;
	public function __construct ($meta=array())
	{
		$this->meta = $meta;
	} // end of __construct
	public function getName ()
	{
		return $this->name;
	} // end of function getName
	public function setMeta ($meta)
	{
		$this->meta = array_merge($this->meta, $meta);
		return $this;
	} // end of function setMeta

	public function addTypeRef ($t)
	{
		array_unshift($this->type_ref, $t);
		return $this;
	} // end of function addTypeRef
	public function getTypeRefs ()
	{
		return $this->type_ref;
	} // end of function addTypeRef
	public function setName ($name)
	{
		$this->name = $name;
	} // end of function setName

	public function create ($args)
	{
		if (array_key_exists('default', $this->meta)) return $this->meta['default'];
		return NULL;
		//return NULL;
	} // end of function create

	public function init ($data)
	{
		$this->meta = $data;
		return $this;
	} // end of method init
}
class TObject extends TAtom
{
	public $fields;

	public function __construct ($meta=array(), $fields=array())
	{
		parent::__construct($meta);
		$this->fields = $fields;
	} // end of __construct
	public function setField ($name, $type)
	{
		$this->fields[$name] = $type;
		$type->meta['name'] = $name;
	} // end of function setField
	public function getField ($name)
	{
		return $this->fields[$name];
	} // end of function getField
	public function getFields ()
	{
		return $this->fields;
	} // end of function getFields

	function getImplement ($implement=NULL)
	{
		$implement_url = isset($implement) ? $implement : (isset($this->meta['implement']) ? $this->meta['implement'] : NULL);
		if (isset($implement_url)) {
			$url = parse_url($implement_url);
			if (isset($url['fragment'])) {
				$script = $url['path'];
				$implement = $url['fragment'];
			} else {
				$script = NULL;
				$implement = $url['path'];
			}
			if (isset($script)) {
				if (is_script($script)) {
					require_once $script;
				} else {
					error(__METHOD__."() script '{$script}' not found");
				}
			}
		} else {
			if ($this->type_ref) {
				foreach ($this->type_ref as $t) {
					if ($implement = $t->getImplement()) {
						break;
					}
				}
			} else {
				$implement = $this->implement;
			}
		}

		return $implement;
	} // end of function getImplement
	public function create ($args, $implement=NULL)
	{
		$type_name = $this->getName();

		$implement = $this->getImplement($implement);
		do {
			if (is_callable($implement)) {
				$o = $implement($this, $args);
				break;
			}
			if (class_exists($implement)) {
				$o = new $implement($args);
				/*
				// -- inherit fields from parents -- {{{
				$fields = $this->getFields();
				$protos = $this->getTypeRefs();
				while ($prot = array_shift($protos)) {
					$protos = array_merge($prot->getTypeRefs());
					$fields = array_merge($prot->getFields(), $fields);
				}
				// 
				// -- inherit fields from parents -- }}}

				foreach ($fields as $fn=>$ft) {
					//if (array_key_exists('default', $ft->meta)) {
						//$o->$fn = $ft->meta['default'];
						//$o->data[$fn] = $ft->create($args);
					//}
				}
				 */
				break;
			}
			if (is_script($implement)) {
				extract($args);
				$o = include $script;
				break;
			}
			error(__METHOD__."() type '{$type_name}': implementation '{$implement}' not found");
		} while (false);
		if (is_object($o)) { $o->__type = $this; }
		return $o;
	} // end of function create

	public function init ($data)
	{
		if (isset($data['fields'])) {
			foreach($data['fields'] as $v) {
				// HACK : 2007-12-06 : lucky
				// если тип не указан, будем считаь, что 'atom'
				// хотя это ошибка
				if (!isset($v['type'])) $v['type'] = 'atom';
				$field_name = $v['name'];
				$field_type = $v['type'];
				if (!isset($v['rec']) || !$v['rec']) { $v['name'] = $this->getName().'.'.$v['name']; }
				$field_type = new_type($v);
				if ($field_type) { $this->setField($field_name, $field_type); }
			}
		}
		return parent::init($data);
	} // end of method init
}
class TCollection extends TObject
{
	public $indexes;
	public $primary_key;

	public function setPrimaryKey ($fields)
	{
		$this->primary_key = $fields;
		return $this;
	} // end of function setPrimaryKey
	public function getPrimaryKey ()
	{
		return $this->primary_key;
	} // end of function getPrimaryKey
	public function getIndexes ()
	{
		return $this->indexes;
	} // end of function getIndex

	public function init ($data)
	{
		$this->indexes     = $data['indexes'];
		$this->primary_key = $data['primary_key'];

		return parent::init($data);
	} // end of method init
}


function new_type_from_doctype ($base_name, $my_name, $doctype_info)
{
	$td = TypeDomain::instance();
	if ($doctype = $td->getType($my_name)) return $doctype;

	$doctype = $td->cloneType($my_name, $base_name);
	if (!$doctype) {
		error(__METHOD__."() type '$base_type' not defined");
		return NULL;
	}

	if (isset($doctype_info['fields'])) {
		foreach($doctype_info['fields'] as $k => $v) {
			$field_type_name = $v['db_type'];
			$field_name      = $k;
			if ($v['rec']) $field_type = new_type_from_doctype($field_type_name, $field_type_name, $v);
			else           $field_type = new_type_from_doctype($field_type_name, $my_name.'.'.$field_name, $v);
			if ($field_type) {
				$doctype->setField($field_name, $field_type);
			}
		}
	}
	if (isset($doctype_info['typename'])) $doctype_info['title'] = $doctype_info['typename'];
	unset($doctype_info['fields']);
	unset($doctype_info['typename']);
	$doctype->setMeta($doctype_info);
	return $doctype;
} // end of function new_type_from_doctype

/*
function error ($msg)
{
	echo $msg."\n";
	die();
} // end of function error
function debug ($msg)
{
	echo $msg."\n";
} // end of function error




$platonica = '/home/lucky/public_html/platonica';
ini_set('include_path', ini_get('include_path').PATH_SEPARATOR."$platonica/lib");





$td = TypeDomain::instance();
$td->addLoader(new KarniTypeLoader("$platonica/etc/type.d"));
$td->addLoader(new DocTypeLoader("$platonica/etc/conf.d/doctype.php"));

$td->setType('atom'        , new TAtom());
$td->setType('object'      , new TObject());
$td->setType('collection'  , new TCollection(array()));
$td->cloneType('string'    , 'atom');
$td->cloneType('integer'   , 'atom');
$td->cloneType('bool'      , 'atom');
$td->aliasType('boolean'   , 'bool');
$td->cloneType('text'      , 'atom');
$td->cloneType('datetime'  , 'atom');
$td->cloneType('timestamp' , 'atom');

$t = get_type('cnode', array());
//$o = new_inst('cnode');
var_dump($t, $o);

//$t = new_type(array(
//	'name' => 'ffff',
//	'type'=>'cnode',
//	'fields' => array(array('name'=>'tags', 'type'=>'string', 'default'=>'123')
//)));
//var_dump($t);
//var_dump(has_type('ffff', 'doc'));
//$o = new_inst('ffff', array(), 'foo');
 */
